package controller.gui;

import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.lang.reflect.InvocationTargetException;

import javax.swing.JButton;

import tool.ClassValue;
import tool.MainConstructorNotFoundException;
import tool.dialog.JConstructorDialog;
import tool.dialog.JConstructorUpgradedDialog;
import tool.dialog.KeyDialog;

import controller.Controller;
import controller.NullStaticObjectException;
import controller.ObjectAdderInformationContainer;
import controller.StandardIOController;
import controller.StratumLevelUser;
import controller.MapElementToAddConveyor;
import controller.adder.BackgroundAdder;
import controller.adder.MapLayerAdder;
import controller.adder.MapObjectAdder;
import controller.gui.menubar.NewMapController;
import controller.states.UserObjectToAddListener;

import errors.StratumNotFoundException;
import event.numberevent.NumberEvent;
import event.numberevent.Range;
import event.numberevent.StateNumber;
import event.timerevent.Timer;
import event.timerevent.TimerEvent;
import event.timerevent.TimerListener;
import gui.BoxElement;
import gui.Display;

import mapelements.MapElement;
import mapelements.Renderer;
import mapelements.background.Background;
import mapelements.foreground.Foreground;
import mapelements.foreground.MapLayer;
import mapelements.foreground.MapObject;


/**Rappresenta il Controller che si occupa di gestire gli eventi
 * per il corretto inserimento dei Background/Foreground nel
 * Display. Tale corretto inserimento dipende in grosso modo
 * dal tipo di classe che e' stata importata dal ClassLoader;
 * <p>In generale qualunque sia il modo di inserire oggetti nella
 * mappa le azioni da eseguire sono:
 * 
 * @author Koldar
 * @version 1.0
 */
public final class InsertionDisplayController extends StandardIOController implements TimerListener,MapElementToAddConveyor,UserObjectToAddListener, MouseMotionListener, MouseWheelListener,StratumLevelUser,ActionListener{

	/**indica la classe che contiene il codice su come inserire un determinato tipo di oggetto*/
	private ObjectAdderInformationContainer<?> adder;
	/**rappresenta il display ascoltato da questo controller*/
	private Display listenedDisplay;
	/**rappresenta la sveglia che dice dopo quanto tempo aggiornare il Display dopo le modifiche fatte sui SelectionBox. Per default
	 * il timer è settato su 0.5 secondi*/
	private Timer mapDelay;
	/**indica quale stato il <tt>userObjectToAddState</tt> deve assumere quando il timer squilla*/
	private int stateToSetInTimerRung;
	/**indica l'ultima chiave che l'utente ha deciso di usare come chiave per un Foreground. Questa chiave è solo quella <strong>base</strong>,
	 * cioé quella senza le eventuali aggiunte di numeri finali per rispettare l'unicità delle chiavi nella mappa
	 */
	private String lastBaseUserKey;
	/**la finestra che viene controllata dal controller*/
	private Frame frame;
	/**rappresenta il concetto del MapElement che l'utente vuole inserire nella mappa. Questo oggetto manca però del renderer, immagazzinato in {@link #rendererToAdd}*/ 
	private Class<MapElement> conceptToAdd;
	/**rappresenta il renderer del MapElement che l'utente vuole inserire nella mappa. Questo oggetto manca però del concept, immagazzinato in {@link #conceptToAdd}*/
	private Class<Renderer> rendererToAdd;
	/**rappresentano i pulsanti che indicano la volontà dell'utente di inserire il mapElement scelto nella mappa*/
	private JButton[] activateInsertionButton;
	
	/**costurisce un nuovo controllore che si preoccupa di inserire i MapElement nel display
	 * 
	 * @param _frame indica l'applicazione che ospita il display in cui la mappa viene visualizzata
	 * @param towire indica il display in cui la mappa viene visualizzata
	 * @param activateInsertion indica un pulsante che, se premuto, avverte il DIsplayController che l'utente 
	 * ha deciso <strong>definitivamente</strong> il mapElement da inserire e vuole intraprendere la procedura
	 * di inserzione MapElement.
	 */
	public InsertionDisplayController(Frame _frame,Display towire,JButton ...activateInsertion){
		super();
		this.mapDelay=new Timer(500,1);
		this.mapDelay.addTimerListener(this);
		this.lastBaseUserKey="";
		this.stateToSetInTimerRung=this.START;
		this.frame=_frame;
		this.listenedDisplay=towire;
		this.conceptToAdd=null;
		this.rendererToAdd=null;
		this.activateInsertionButton=new JButton[activateInsertion.length];
		for (int i=0;i<activateInsertion.length;i++){
			this.activateInsertionButton[i]=activateInsertion[i];
			this.activateInsertionButton[i].addActionListener(this);
		}
		this.currentmap.addMapListener(this.listenedDisplay);
		this.listenedDisplay.setMap(currentmap);
		this.listenedDisplay.addMouseMotionListener(this);
		this.listenedDisplay.addMouseWheelListener(this);
		this.mapElementToAddState.addNumberListener(this);
	}
	
	/**aggiunge il {@link #listenedDisplay} alla lista di oggetti che devono notificare questo controllore
	 * 
	 */
	public void addInterestInDisplay(){
		this.addStandardIOListener(listenedDisplay);
	}
	
	/**rimuove il {@link #listenedDisplay} alla lista di oggetti che devono notificare questo controllore
	 * 
	 */
	public void removeInterestInDisplay(){
		this.removeStandardIOListener(listenedDisplay);
	}
	
	/**termina tutti i thread utilizzati da questa classe per l'inserimento dei MapElement nella mappa
	 * 
	 */
	public void terminateAuxiliarThreads(){
		this.adder.terminateAuxiliarThreads();
		this.mapDelay.SuspendTimer();
	}
	
	/**
	 * @param conceptToAdd the conceptToAdd to set
	 */
	public void setConceptToAdd(Class<MapElement> conceptToAdd) {
		this.conceptToAdd = conceptToAdd;
		this.manageMapElementToAddState();
		this.updateLog();
	}

	/**assegna un valore al <tt>mapElementToAddState</tt> a seconda del valore nullo o non nullo delle 2 variabili
	 * {@link #conceptToAdd} e {@link #rendererToAdd}
	 * 
	 */
	private void manageMapElementToAddState(){
		if (this.rendererToAdd==null){
			if (this.conceptToAdd==null){//RENDERER: NULL CONCEPT: NULL
				this.mapElementToAddState.setValue(this.START);
			}else{//RENDERER: NULL CONCEPT: NOT NULL
				this.mapElementToAddState.setValue(this.ONLYCONCEPT);
			}
		}else{
			if (this.conceptToAdd==null){//RENDERER: NOT NULL CONCEPT: NULL
				this.mapElementToAddState.setValue(this.ONLYRENDERER);
			}else{//RENDERER: NOT NULL CONCEPT: NOT NULL
				this.mapElementToAddState.setValue(this.MAPELEMENTUPDATED);
				this.mapElementToAddState.setValue(this.MAPELEMENTCHOOSEN);
				//prima si aggiorna il mapelement, poi viene scelto
			}
		}
	}
	/**aggiorna il log panel circa le scelte fatte dall'utente
	 * 
	 */
	private void updateLog(){
		this.log.setText(
				"renderer choosen: "+(this.rendererToAdd==null?"NULL":this.rendererToAdd.getSimpleName())+
				"; concept choosen: "+(this.conceptToAdd==null?"NULL":this.conceptToAdd.getSimpleName()));
	}

	/**
	 * @param rendererToAdd the rendererToAdd to set
	 */
	public void setRendererToAdd(Class<Renderer> rendererToAdd) {
		this.rendererToAdd = rendererToAdd;
		this.manageMapElementToAddState();
		this.updateLog();
	}

	/**quando l'utente ha iniziato la procedura dell'inserzione del MapElement
	 * devono valere queste condizioni:
	 * <ul>
	 *  <li>il display non deve essere modificato o essere interattivo con l'utente</li>
	 *  <li>il {@link #rendererToAdd} deve essere NULL</li>
	 *  <li>il {@link #conceptToAdd} deve essere NULL</li>
	 *  <li>il {@link #activateInsertionButton} deve essere disattivato</li>
	 * </ul>
	 * @param arg0
	 */
	@Override
	public void startInsertion(NumberEvent<Integer> arg0) {
		try{
			this.terminateAuxiliarThreads();
		}catch (NullPointerException adderNull){}
		this.removeInterestInDisplay();
		this.rendererToAdd=null;
		this.conceptToAdd=null;
		for (int i=0;i<this.activateInsertionButton.length;i++){
			this.activateInsertionButton[i].setEnabled(false);
		}
	}
	
	@Override
	public void rendererChoosen(NumberEvent<Integer> arg0) {
		
	}

	@Override
	public void conceptChoosen(NumberEvent<Integer> arg0) {
		
	}
	
	@Override
	public void mapElementUpdated(NumberEvent<Integer> arg0) {
	}

	/**in caso in cui l'utente abbia deciso l'elemento della mappa da inserire, cabla il display con
	 * l'input output base dell'utente, quindi carica la classe di interfaccia utente che indica come il
	 * software deve rispondere agli input dell'utente
	 * 
	 */
	@Override
	public void mapElementChoosen(NumberEvent<Integer> arg0) {
		for (int i=0;i<this.activateInsertionButton.length;i++){
			this.activateInsertionButton[i].setEnabled(true);
		}
	}


	@Override
	public void mouseWheelMoved(MouseWheelEvent arg0) {
		// TODO Auto-generated method stub
	}

	@Override
	public void mouseDragged(MouseEvent arg0) {
		this.mouseMoved(arg0);
	}

	@Override
	public void mouseMoved(MouseEvent arg0) {
	}


	/**il timer viene attivato solo in caso in cui l'Element da inserire sia un bvackground. In tale caso
	 * il background viene inserito nella mappa con un ritardo pari a quello scelto da {@link #mapDelay}
	 * 
	 */
	@Override
	public void TimerRung(TimerEvent arg0) {
		this.mapElementToAddState.setValue(this.stateToSetInTimerRung);
		//resetta il timer
		this.mapDelay.SuspendTimer();
	}


	@Override
	public void TimerPaused(TimerEvent arg0) {
	}


	@Override
	public void TimerSuspended(TimerEvent arg0) {
	}
	
	public void finalize(){
		this.mapDelay.SuspendTimer();
	}


	/**data una classe, tenta di istanziare tale classe usando il JConstructorDialog
	 * 
	 * @param toInstance la classe da istanzializzare
	 * @return un'istanza della classe toInstance
	 */
	private Object instanceClass(Class toInstance){
		//***************** istanzializzazione della classe ***************************
		Object instance=null;
		try {
			instance=toInstance.newInstance();
			return instance;
		} catch (InstantiationException | IllegalAccessException error1) {
			//la classe non possiede un costruttore vuoto immediatamente utilizzabile. devo usare il JConstructorDialog!
			try {
				JConstructorDialog dialog=new JConstructorUpgradedDialog(this.frame,toInstance,this.listenedDisplay);
				instance=dialog.getMainConstructor().newInstance(dialog.getParameterlist());
				return instance;
			} catch (MainConstructorNotFoundException error) {
				//in caso non ci sia un MainConstructor... avverto l'utente e finisco
				Controller.log.setText(error.getMessage());
			} catch (NullStaticObjectException error) {
				//in caso getApplication sia NULL
				error.printStackTrace();
			} catch (InstantiationException error) {
				// TODO Auto-generated catch block
				error.printStackTrace();
			} catch (IllegalAccessException error) {
				// TODO Auto-generated catch block
				error.printStackTrace();
			} catch (IllegalArgumentException error) {
				//in caso l'utente esca dal JConstructorDialog
			} catch (InvocationTargetException error) {
				// TODO Auto-generated catch block
				error.printStackTrace();
			}
		}
		return null;
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		//***************** istanzializzazione del Renderer scelto *******************
		Renderer renderer=(Renderer)this.instanceClass(rendererToAdd);
		//***************** istanalializzazione del Concept scelto *******************
		MapElement element=(MapElement)this.instanceClass(conceptToAdd);
		//***************** creazione del MapElement da inserire **************************
		if ((renderer==null)||(element==null)){
			this.log.setText("operation interrupted: renderer or concept null");
			return;
		}
		element.setRenderer(renderer);
		//***************** valutazione di cosa sia l'oggetto
		if (element instanceof Background){
			this.adder=new BackgroundAdder(this.listenedDisplay,(Background)element,Controller.getCurrentmap());
		}
		if (element instanceof Foreground){
			//***************** richiesta della chiave ***********************************
			KeyDialog dialog=new KeyDialog(this.frame,this.getCurrentmap().getKeys(),this.lastBaseUserKey);
			if (dialog.getUserKey()==null){//non si ottenuta una chiave dall'utente
				this.log.setText("operation interrupted");
				return;
			}
			//***************** ottenimento della chiave ****************************
			this.lastBaseUserKey=dialog.getUserKey();
			this.addInterestInDisplay();
			if (element instanceof MapLayer){
				this.adder=new MapLayerAdder(this.listenedDisplay,(MapLayer)element,Controller.getCurrentmap(),this.stratum.getValue(),this.lastBaseUserKey);
			}else{
				this.adder=new MapObjectAdder(this.listenedDisplay,(MapObject)element,Controller.getCurrentmap(),this.stratum.getValue(),this.lastBaseUserKey);
			}
		}
	}
	
	@Override
	public void doubleClick(MouseEvent arg0) {
		try {
			this.adder.doubleClick(arg0);
		} catch (StratumNotFoundException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void leftClick(MouseEvent arg0) {
		try {
			this.adder.leftClick(arg0);
		} catch (StratumNotFoundException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void rightClick(MouseEvent arg0) {
		try {
			this.adder.rightClick(arg0);
		} catch (StratumNotFoundException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void unknownClick(MouseEvent arg0) {
		try {
			this.adder.unknownClick(arg0);
		} catch (StratumNotFoundException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void enterTyped(KeyEvent arg0) {
		try {
			this.adder.enterTyped(arg0);
		} catch (StratumNotFoundException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void escTyped(KeyEvent arg0) {
		try {
			this.adder.escTyped(arg0);
		} catch (StratumNotFoundException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void unkwnownTyped(KeyEvent arg0) {
		try {
			this.adder.unkwnownTyped(arg0);
		} catch (StratumNotFoundException e) {
			e.printStackTrace();
		}
	}
	
}
